Chapter 31
Multimedia and MFC

by Michael Morrison

In This Chapter

  Multimedia Fundamentals 1118
  The DirectX Media Player Control 1081
  Using the Media Player Control 1122
  Playing Sound 1131

Multimedia has become a critical part of many Windows applications. There was a time when multimedia applied only to games and special multimedia presentations. However, multimedia content is now used extensively in business applications such as TurboTax and Microsoft Money, not to mention the many uses of multimedia on the Web. As an MFC developer, you should be prepared to leverage some degree of multimedia content in applications that you develop.

This chapter explores multimedia and how it affects MFC application development. You learn about the DirectX Media Player control, as well as how to use DirectSound to add multimedia sound capabilities to applications.

Multimedia Fundamentals

Multimedia is probably one of the most overused terms in the history of computing. Interestingly enough, many users don’t even understand exactly what the term means. Multimedia refers to the different types of content displayed and interacted with in an application. In a general sense, any application content that contains audio or video elements could be considered multimedia content. However, I’d like to make a clearer distinction between multimedia content and other application content.

In general, multimedia content can be defined as audio, video, or other content that is time based, which means that the content changes over time. This definition of multimedia eliminates static content such as still images. This isn’t meant to lessen the importance of images in Windows applications. Rather, I’m trying to get the point across that multimedia content encompasses a specific type of data. Keep in mind that static images could still fit in to a multimedia presentation as components of an animation that is time based. However, this definition of multimedia requires that the images be changed or altered over time.

Practically all games are considered multimedia applications because of their heavy use of multimedia content. There are also multimedia applications that ship with Windows 95/98. The CD Player and Sound Recorder applications are multimedia applications used to play CDs and record audio clips. You could even think of Microsoft Word and some other productivity applications as multimedia applications because it’s possible to embed audio and video clips within Word documents. Web browsers are also very good examples of multimedia applications because they can be used to view a wide range of media content.

Chapter 30, “MFC and DirectX,” introduces you to DirectX, which is designed to allow applications to simultaneously display and manipulate 2D and 3D graphics, animation, video, and surround sound, as well as support advanced user input devices. DirectX figures heavily into Windows multimedia content. In fact, DirectX forms the basis of every Windows multimedia technology. This wasn’t always the case—earlier versions of Windows supported different types of multimedia content with completely different technologies. DirectX serves as the integration of all the tools and programming interfaces for the playback and capture of multimedia content.

The DirectX Media Player Control

One particular facet of DirectX that is extremely valuable for working with media content is the DirectX Media Player control, which is an ActiveX control that is part of the DirectX Media layer. The Media Player control is based on the DirectShow component of the DirectX Media layer and is used solely for the playback of multimedia content. The Media Player control acts as a universal player for most standard multimedia formats, including both streaming and nonstreaming media content. This makes the Media Player control extremely versatile; you can use the control in both applications and Web pages.

Media Formats Supported by the Media Player Control

The Media Player control supports the following streaming media formats, which are typically used for playback over a network:

  Advanced Streaming Format (ASF)
  Video On Demand (VOD)
  Moving Picture Experts Group standard 1, 2, and 3 (MPEG-1, MPEG-2, and MPEG-3)
  RealAudio (RA) and RealVideo (RV)


Note:  

Advanced Streaming Format (ASF) is Microsoft’s standard streaming media format recommended for use with the Media Player control. An ASF file is capable of storing both audio and video and is specially designed to be used over networks such as the Internet. ASF media content is delivered as a continuous flow of data with minimal wait time before playback begins.


The Media Player control also supports the following nonstreaming media formats, which are typically used for local playback:

  Audio-Video Interleaved (AVI)
  QuickTime (MOV)
  Musical Instrument Digital Interface (MIDI)
  Indeo 5
  Waveform Audio (WAV)
  Sound File (SND)
  UNIX audio (AU)
  Audio Interchange File Format (AIFF)

Inside the Media Player Control

As with all ActiveX controls, you interact with the Media Player control by setting properties and calling member functions on the control. You can also respond to events that are generated by the control. Because the control is implemented as an ActiveX control, you can interact with it from applications written in a variety of languages or from a scripting language in a Web page. Using the properties and member functions exposed by the control, you can

  Control playback with operations such as start, stop, pause, rewind, and fast-forward
  Adjust the volume level or mute the audio entirely
  Monitor the performance of the media stream
  Retrieve media content information such as the author and title

The Media Player control is used as the basis for the Windows Media Player application that ships with Windows 98. The Windows Media Player application uses the standard user interface provided by the Media Player control and enables you to open and play the different types of media supported by the control. This application is a good example of how the Media Player control can be used to play back media content. Figure 31.1 shows the Windows Media Player application in action.


Figure 31.1  The Windows Media Player application that uses the Media Player control.


Note:  

For more extensive information on the DirectX Media Player control, check out Microsoft’s Windows Media Web site at http://www.microsoft.com/windows/windowsmedia/.




The CMediaPlayer Class

Although you can certainly interact with the Media Player control directly from MFC using its COM interfaces, this can be messy. The DirectX Media SDK (Software Development Kit) includes an MFC class named CMediaPlayer that serves as a C++ wrapper around the Media Player control. The CMediaPlayer class doesn’t really provide any functionality of its own; it simply hides the details of making function calls through the ActiveX control’s COM interfaces. Even so, the CMediaPlayer class is very convenient and keeps you from having to hassle with the calling conventions required when interacting with ActiveX controls.


Note:  

Because the CMediaPlayer wrapper class isn’t part of a formal class library, you have to include the header (MediaPlayer.h) and source code (MediaPlayer.cpp) files in any projects that utilize the class.


Following are a few of the most commonly used member functions defined in the CMediaPlayer class:

  GetVolume()—Gets the volume of the control
  SetVolume()—Sets the volume of the control
  SetFileName()—Sets the filename of the media clip to be played
  Play()—Plays the current media clip
  Pause()—Pauses the current media clip
  Stop()—Stops the current media clip
  GetCurrentPosition()—Gets the current playback position of the current media clip
  SetCurrentPosition()—Sets the current playback position of the current media clip

This is only a very small subset of the member functions available for use in the CMediaPlayer class. In fact, the CMediaPlayer class defines more than 150 member functions that are used to control a wide range of media playback parameters. To keep things simple, let’s stick to the basic media player functions.

Assuming that you already have a pointer to a CMediaPlayer object in the variable pMediaPlayer, following is an example of setting the player to an AVI video clip named KingKong.avi:

pMediaPlayer->SetFileName(“KingKong.avi”);

To start playing the video clip, you simply call the Play() member function, like this:

pMediaPlayer->Play();

You can then pause or stop the playback of the video clip by calling the Pause() or Stop() member functions.

Using the Media Player Control

Although the Windows Media Player application is very powerful and serves as a great all-purpose media player, it doesn’t reveal much about using the Media Player control. So, let’s work through the development of an MFC media player application named Projector that uses the Media Player control. You’ll use the CMediaPlayer wrapper class to access the functionality of the Media Player control.

The Projector application is intended to function somewhat like a film projector. You load a “film” (media clip) and then you can play it, pause it, or stop it; stopping a media clip automatically results in it being rewound. Because the Projector application uses the Media Player control, it supports all kinds of media types, including both streaming and nonstreaming audio and video.

The Projector application is structured as a dialog-based application, which means that the main frame window is created from a dialog resource. This makes it possible to construct the application’s main frame window as a dialog box containing the Media Player control and a few buttons. This is a convenient approach because you can lay out the application user interface using the Visual C++ dialog editor.

Because the main frame window for the Projector application is based on a dialog box resource, resources play an extremely important role in the application. Listing 31.1 shows the Resource.h header file for Projector, which contains the application’s resource identifiers.

Listing 31.1 The Resource.h Header File for Projector


//-----------------------------------------------------------------
// Strings                    Range : 1000 - 1999
//-----------------------------------------------------------------
#define IDR_PROJECTOR         1000

//-----------------------------------------------------------------
// Commands                   Range : 2000 - 2999
//-----------------------------------------------------------------
#define ID_ABOUT              2000

//-----------------------------------------------------------------
// Dialog Boxes               Range : 3000 - 3999
//-----------------------------------------------------------------
#define IDD_ABOUTBOX          3000
#define IDD_PROJECTORBOX      3001

//-----------------------------------------------------------------
// Controls                   Range : 4000 - 4999
//-----------------------------------------------------------------
#define ID_PB_LOAD            4000
#define ID_PB_PLAY            4001
#define ID_PB_PAUSE           4002
#define ID_PB_STOP            4003
#define ID_MP_PLAYER          4004

The IDD_PROJECTORBOX identifier is used to identify the dialog box resource that the main frame window is based on. This dialog box resource contains a Media Player control and a series of buttons used to manipulate the control, as you’ll soon see. The buttons and the Media Player control are identified by the control constants near the end of the header file.

The resources for the Projector application are defined in the Projector.rc resource script, which is shown in Listing 31.2.

Listing 31.2 The Projector.rc Resource Script for Projector


//-----------------------------------------------------------------
// Inclusions
//-----------------------------------------------------------------
#include “AfxRes.h”
#include “Resource.h”

//-----------------------------------------------------------------
// Icons
//-----------------------------------------------------------------
IDR_PROJECTOR ICON “Projector.ico”

//-----------------------------------------------------------------
// Dialog Boxes
//-----------------------------------------------------------------
IDD_ABOUTBOX DIALOG 0, 0, 217, 55
CAPTION “About Projector”
STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
FONT 8, “MS Sans Serif”
BEGIN
  ICON          IDR_PROJECTOR, IDC_STATIC, 11, 17, 20, 20
  LTEXT         “Projector Version 1.0”, IDC_STATIC, 40, 10,
                  119, 8, SS_NOPREFIX
  LTEXT         “Copyright (c)1998 Michael Morrison”, IDC_STATIC,
                  40, 25, 119, 8
  DEFPUSHBUTTON “OK”, IDOK, 178, 7, 32, 14, WS_GROUP
END
IDD_PROJECTORBOX DIALOGEX 0, 0, 311, 186
CAPTION “Projector”
STYLE DS_MODALFRAME | WS_MINIMIZEBOX | WS_POPUP | WS_VISIBLE |
  WS_CAPTION | WS_SYSMENU
EXSTYLE WS_EX_APPWINDOW
FONT 8, “MS Sans Serif”
BEGIN
  DEFPUSHBUTTON “&Load...”, ID_PB_LOAD, 254, 7, 50, 14
  PUSHBUTTON    “&Play”, ID_PB_PLAY, 254, 25, 50, 14
  PUSHBUTTON    “P&ause”, ID_PB_PAUSE, 254, 43, 50, 14
  PUSHBUTTON    “&Stop”, ID_PB_STOP, 254, 61, 50, 14
  PUSHBUTTON    “E&xit”, IDCANCEL, 254, 79, 50, 14
  CONTROL       “”, ID_MP_PLAYER,
    “{22D6F312-B0F6-11D0-94AB-0080C74C7E95}”, WS_TABSTOP, 6, 7,
    242, 172
END
IDD_PROJECTORBOX DLGINIT
BEGIN
  ID_MP_PLAYER, 0x376, 344, 0
  0x0000, 0x0000, 0x0001, 0x0000, 0x2584, 0x0000, 0x1cf0, 0x0000,
    0x0003,
  0xffff, 0xffff, 0x000b, 0x0000, 0x000b, 0xffff, 0x000b, 0xffff,
    0x000b,
  0xffff, 0x000b, 0xffff, 0x0000, 0x0002, 0x0000, 0x0000, 0x000b,
    0x0000,
  0x0003, 0x0000, 0x0000, 0x0008, 0x0002, 0x0000, 0x0000, 0x0005,
    0x0000,
  0x0000, 0x0000, 0x4014, 0x0008, 0x0002, 0x0000, 0x0000, 0x000b,
    0xffff,
  0x0003, 0x0000, 0x0000, 0x0005, 0x0000, 0x0000, 0x0000, 0xbff0,
    0x0003,
  0x0000, 0x0000, 0x0008, 0x0002, 0x0000, 0x0000, 0x0013, 0x0000,
    0x0000,
  0x0013, 0xffff, 0x00ff, 0x0003, 0x0000, 0x0000, 0x0003, 0x0000,
    0x0000,
  0x000b, 0xffff, 0x000b, 0xffff, 0x000b, 0xffff, 0x000b, 0x0000,
    0x000b,
  0xffff, 0x0008, 0x0002, 0x0000, 0x0000, 0x000b, 0xffff, 0x0003,
    0xffff,
  0xffff, 0x000b, 0x0000, 0x0003, 0x0001, 0x0000, 0x000b, 0x0001,
    0x0005,
  0x0000, 0x0000, 0x0000, 0x3ff0, 0x0008, 0x0002, 0x0000, 0x0000,
    0x0008,
  0x0002, 0x0000, 0x0000, 0x0008, 0x0006, 0x0000, 0x002d, 0x0031,
    0x0000,
  0x0005, 0x0000, 0x0000, 0x0000, 0xbff0, 0x0005, 0x0000, 0x0000,
    0x0000,
  0xbff0, 0x000b, 0xffff, 0x000b, 0xffff, 0x000b, 0x0000, 0x000b,
    0x0000,
  0x000b, 0x0000, 0x000b, 0xffff, 0x000b, 0xffff, 0x000b, 0x0000,
    0x000b,
  0x0000, 0x000b, 0x0000, 0x000b, 0x0000, 0x000b, 0x0000, 0x000b,
    0x0000,
  0x000b, 0x0000, 0x000b, 0x0000, 0x000b, 0x0000, 0x0003, 0x0000,
    0x0000,
  0x0013, 0x0000, 0x0000, 0x000b, 0xffff, 0x0003, 0xfda8, 0xffff,
    0x000b,
  0x0000,
  0
END



The IDD_PROJECTOR dialog box resource defines the dialog box that will serve as the main application window. This dialog box contains a series of buttons used to manipulate the media player, along with the actual Media Player control. Unlike other types of controls, ActiveX controls require you to initialize them before they can be used. Because they often have lots of different attributes that need to be initialized, they require the use of a dialog initialization structure. This is the big structure consisting of hexadecimal numbers that you see toward the end of the Projector.rc resource script.

Fortunately, you don’t have to worry about what the numbers mean in the dialog initialization structure—they are automatically generated by the Visual C++ dialog editor. The IDD_PROJECTOR dialog box was created using the Visual C++ dialog editor and is shown in Figure 31.2.


Figure 31.2  The IDD_PROJECTOR dialog box in the Visual C++ dialog editor.

Notice in Figure 31.2 that the Media Player control is selected and the Properties window is displayed. The Properties window shows the different properties of the Media Player control, which are then written to the dialog initialization structure when the resource script is saved.

As you already know, the main frame window of the Projector application is implemented as a dialog box. Listing 31.3 contains the declaration of the CProjectorDlg class.

Listing 31.3 The ProjectorDlg.h Header File for Projector


#ifndef __PROJECTORDLG_H__
#define __PROJECTORDLG_H__

//-----------------------------------------------------------------
// Inclusions
//-----------------------------------------------------------------
#include “Resource.h”
#include “MediaPlayer.h”

//-----------------------------------------------------------------
// CProjectorDlg Class - Projector Dialog Object
//-----------------------------------------------------------------
class CProjectorDlg : public CDialog {
  // Member Constants
  enum { IDD = IDD_PROJECTORBOX };

  // Member Data
protected:
  HICON         m_hIcon;
  CMediaPlayer* m_pMediaPlayer;

  // Public Constructor(s)/Destructor
public:
                  CProjectorDlg(CWnd* pParent = NULL);

  // Public Member Functions
public:
  virtual BOOL    OnInitDialog();

  // Message Handlers
public:
  afx_msg HCURSOR OnQueryDragIcon();
  afx_msg void    OnPaint();
  afx_msg void    OnSysCommand(UINT nID, LPARAM lParam);
  afx_msg void    OnPBLoad();
  afx_msg void    OnPBPlay();
  afx_msg void    OnPBPause();
  afx_msg void    OnPBStop();

  // Message Map & Runtime Support
protected:
  DECLARE_MESSAGE_MAP()
};

#endif

The CProjectorDlg class defines two member variables, m_hIcon and m_pMediaPlayer. The m_hIcon member stores the application’s icon, which is necessary because dialog boxes don’t typically have icons associated with them. Thus, you must manually support an icon in the application using the m_hIcon member variable. The m_pMediaPlayer member stores a pointer to the Media Player control in the dialog box window.

Four of the message handlers (PBLoad(), PBPlay(), PBPause(), and PBStop()) in CProjectorDlg are called in response to buttons on the dialog box. These buttons control the Media Player control through their corresponding message handlers. Listing 31.4 contains the complete source code for the CProjectorDlg class, which includes implementations of these button message handlers.

Listing 31.4 The ProjectorDlg.cpp Source Code File for Projector


//-----------------------------------------------------------------
// Inclusions
//-----------------------------------------------------------------
#include “StdAfx.h”
//-----------------------------------------------------------------
#include <CommDlg.h>
#include “ProjectorDlg.h”
#include “AboutDlg.h”

//-----------------------------------------------------------------
// MFC Debugging Support
//-----------------------------------------------------------------
#ifdef _DEBUG
#undef THIS_FILE
static char BASED_CODE THIS_FILE[] = __FILE__;
#endif


//-----------------------------------------------------------------
// Message Map & Runtime Support
//-----------------------------------------------------------------
BEGIN_MESSAGE_MAP(CProjectorDlg, CDialog)
  ON_WM_QUERYDRAGICON()
  ON_WM_PAINT()
  ON_WM_SYSCOMMAND()
  ON_BN_CLICKED(ID_PB_LOAD, OnPBLoad)
  ON_BN_CLICKED(ID_PB_PLAY, OnPBPlay)
  ON_BN_CLICKED(ID_PB_PAUSE, OnPBPause)
  ON_BN_CLICKED(ID_PB_STOP, OnPBStop)
END_MESSAGE_MAP()

//-----------------------------------------------------------------
// Public Constructor(s)/Destructor
//-----------------------------------------------------------------
CProjectorDlg::CProjectorDlg(CWnd* pParent) :
  CDialog(CProjectorDlg::IDD, pParent) {
  // Load the application icon
  m_hIcon = AfxGetApp()->LoadIcon(IDR_PROJECTOR);
}

//-----------------------------------------------------------------
// Public Member Functions
//-----------------------------------------------------------------
BOOL CProjectorDlg::OnInitDialog() {
  CDialog::OnInitDialog();

  // Add the About menu item to the system menu
  CMenu* pSysMenu = GetSystemMenu(FALSE);
  if (pSysMenu != NULL) {
    pSysMenu->AppendMenu(MF_SEPARATOR);
    pSysMenu->AppendMenu(MF_STRING, ID_ABOUT, “&About Projector...”);
  }

  // Explicitly set the icon since the main window is a dialog
  SetIcon(m_hIcon, TRUE);

  // Get a pointer to the media player control
  m_pMediaPlayer = (CMediaPlayer *)GetDlgItem(ID_MP_PLAYER);

  return TRUE;
}

//-----------------------------------------------------------------
// Message Handlers
//-----------------------------------------------------------------
HCURSOR CProjectorDlg::OnQueryDragIcon() {
  // Return the cursor for the minimized application
  return (HCURSOR)m_hIcon;
}

void CProjectorDlg::OnPaint() {
  if (IsIconic()) {
    // Manually draw the application icon
    CPaintDC dc(this);
    SendMessage(WM_ICONERASEBKGND, (WPARAM)dc.GetSafeHdc(), 0);

    // Draw the icon centered in the client rectangle
    CRect rc;
    GetClientRect(&rc);
    int x = (rc.Width() - ::GetSystemMetrics(SM_CXICON) + 1) / 2;
    int y = (rc.Height() - ::GetSystemMetrics(SM_CYICON) + 1) / 2;
    dc.DrawIcon(x, y, m_hIcon);
  }
  else
    // Perform default painting
    CDialog::OnPaint();
}

void CProjectorDlg::OnSysCommand(UINT nID, LPARAM lParam) {
  if ((nID & 0xFFF0) == ID_ABOUT) {
    // Display the About dialog box
    CAboutDlg dlgAbout;
    dlgAbout.DoModal();
  }
  else
    // Perform default processing of system menu command
    CDialog::OnSysCommand(nID, lParam);
}

void CProjectorDlg::OnPBLoad() {
  // Get the filename of the movie
  CFileDialog dlg(TRUE, “asf”, “”, OFN_FILEMUSTEXIST,
    “Active Streaming Format (*.asf)|*.asf|” \
    “Active Streaming Redirector (*.asx)|*.asx|” \
    “Audio Video Interleave Format (*.avi)|*.avi|” \
    “RealAudio/RealVideo (*.rm)|*.rm|” \
    “Wave Audio (*.wav)|*.wav|” \
    “All Files (*.*)|*.*||”);

  // Set the movie filename in the media player
  if (dlg.DoModal() == IDOK)
    m_pMediaPlayer->SetFileName(dlg.GetPathName());
}

void CProjectorDlg::OnPBPlay() {
  // Play the movie
  m_pMediaPlayer->Play();
}

void CProjectorDlg::OnPBPause() {
  // Pause the movie
  m_pMediaPlayer->Pause();
}

void CProjectorDlg::OnPBStop() {
  // Stop playing the movie and rewind
  m_pMediaPlayer->Stop();
  m_pMediaPlayer->SetCurrentPosition(0);
}



The code that deals with the Media Player control is isolated in the last four message handlers in CProjectorDlg. The OnPBLoad() message handler first obtains a filename from the user by invoking the File Open common dialog box. It then calls the SetFileName() member function on the Media Player control to set the filename of the media content to be played.

The OnPBPlay() message handler calls the Play() member function on the Media Player control to play the media clip. Likewise, the OnPBPause() message handler calls the Pause() member function on the Media Player control to pause the playback of the clip. Finally, the OnPBStop() message handler calls the Stop() member function to stop the playback of the clip. The OnPBStop() message handler also calls SetCurrentPosition() and passes in 0 to rewind the Media Player to the beginning of the media clip.

Figures 31.3 and 31.4 show the Projector application playing several different types of media content.


Figure 31.3  The Projector application playing the Endorse.asf media file.


Figure 31.4  The Projector application playing the Drill.avi media file.

Playing Sound

Although the Media Player control is useful for playing media content within an application, it isn’t as useful in applications that need to tightly control the playback of multiple sources of media content. More specifically, if you wanted to create a game that played multiple audio clips overlaid at once, the Media Player control wouldn’t be your best option. In this case, you would be better off using DirectSound directly.

If you recall from Chapter 30, DirectSound is the audio portion of DirectX that supports low-latency audio mixing and playback. The latest version of DirectX, version 6.1, also allows you to generate and play interesting music using the DirectMusic API. The next few sections show you how to use DirectSound to play overlaid audio clips much as you would need to do in a game. You also learn how to control the volume of individual audio clips and control the panning between left and right speakers.


Note:  

In case the terminology is new to you, low latency refers to the speed at which an audio clip is played. Latency is the delay between when you initiate the playback of an audio clip programmatically and when the user actually hears the clip. DirectSound has a very low latency, which means that there is a very small delay between the initiation of the playback of a sound and the user hearing the sound.


Working with Waves

Before getting into the specifics of using DirectSound, it’s important for you to understand how audio content is modeled in Windows. The most popular digital sound format used in Windows is the wave format, which is denoted by the .WAV file extension. Windows .WAV files are actually RIFF files, which stands for Resource Interchange File Format. The RIFF format serves as the basis for many of the Windows media file formats.

From the perspective of a multimedia application, waves are just another type of resource, like bitmaps and icons. This means that you can include waves as resources in the resource script for an application.

You can create your own waves using the Sound Recorder application that ships with Windows. Although Sound Recorder is fairly primitive compared to some other commercial wave editors, it does get the job done. If you have a microphone connected to your sound card, you can record just about anything you want. You can also record audio directly from an audio CD in your CD-ROM drive.

Playing Waves

Regardless of how you create wave files, you can use the Win32 PlaySound() function to play them. You’ll use the PlaySound() function in a moment when you create an MFC class that encapsulates a wave audio clip. Following is the prototype for the PlaySound() function:

BOOL PlaySound(LPCSTR pszSound, HMODULE hmod, DWORD fdwSound);

The first parameter to the PlaySound() function, pszSound, is the name of the wave audio clip, which can be the name of a wave file, the name of a wave resource, or a pointer to a wave image in memory. In the case of playing a wave resource, the second parameter, hmod, is the module instance handle where the resource is located. Otherwise, you can pass NULL as this parameter. The last parameter, fdwSound, specifies flags that determine how the sound is played. Table 31.1 describes the flags supported by the PlaySound() function.

Table 31.1 Flags Supported by the PlaySound() Win32 API Function

Flag Description

SND_FILENAME Specifies that the pszSound parameter is a wave filename.
SND_RESOURCE Specifies that the pszSound parameter is a wave resource identifier.
SND_MEMORY Specifies that the pszSound parameter points to a wave image in memory.
SND_ASYNC Plays the sound asynchronously, which means that the function returns immediately after starting the playing of the sound.
SND_SYNC Plays the sound synchronously, which means that the function doesn’t return until the sound finishes playing.
SND_LOOP Plays the sound repeatedly until it is explicitly stopped; looped sounds must be asynchronous, which means that you must use the SND_ASYNC flag with SND_LOOP.
SND_NOSTOP Specifies that the sound won’t interrupt any other sound that is already playing; the sound won’t be played if another sound is being played.
SND_NODEFAULT Specifies that the default system sound won’t be played if the wave sound isn’t located.



This table alludes to a few interesting points regarding the PlaySound() function. First, the PlaySound() function can only be used to play one sound at a time. Secondly, the PlaySound() function will play the default system event sound if the specified wave sound cannot be located. The SND_NODEFAULT flag can be used to circumvent this default behavior.


Note:  

The PlaySound() function is part of the original Windows multimedia support and isn’t technically part of DirectX. However, it is useful in situations where you don’t need the extensive audio capabilities of DirectSound.


Following is an example of playing a looped wave file asynchronously using the PlaySound() function:

::PlaySound(“Siren.wav”, NULL, SND_NODEFAULT | SND_ASYNC | SND_LOOP);

The CWave Class

Although the PlaySound() function is useful in some situations, it can’t compare to the rich audio features offered by DirectSound. The good news is that you still use waves when working with DirectSound. However, it is necessary to extract the wave data from a wave in order to play the wave using DirectSound. This involves digging into a wave and navigating through it to extract wave data.

To make using waves easier in MFC, it is helpful to create an MFC class, CWave, that encapsulates the functionality of a wave. This class could actually serve two purposes:

1.  Provide a high-level means of playing wave audio via the PlaySound() Win32 API function
2.  Provide a low-level means of mixing wave audio with advanced playback capabilities via DirectSound

Not surprisingly, supporting the PlaySound() Win32 API function in the CWave class is very straightforward. Supporting DirectSound, however, is not so easy. Listing 31.5 contains the declaration of the CWave class, which gives you an idea about the member functions that you can use to work with waves.

Listing 31.5 The Wave.h Header File for the CWave Class


#ifndef __WAVE_H__
#define __WAVE_H__
//-----------------------------------------------------------------
// Inclusions
//-----------------------------------------------------------------
#include <MMSystem.h>

//-----------------------------------------------------------------
// CWave Class - Wave Object
//-----------------------------------------------------------------
class CWave : public CObject {
  // Public Constructor(s)/Destructor
public:
          CWave();
          CWave(const CString& sFileName);
          CWave(UINT uiResID, HMODULE hmod =
            AfxGetInstanceHandle());
  virtual ~CWave();

  // Public Methods
public:
  BOOL    Create(const CString& sFileName);
  BOOL    Create(UINT uiResID, HMODULE hmod =
            AfxGetInstanceHandle());
  BOOL    IsValid() const { return (m_pImageData ? TRUE :
            FALSE); };
  BOOL    Play(BOOL bAsync = TRUE, BOOL bLooped = FALSE) const;
  BOOL    GetFormat(WAVEFORMATEX& wfFormat) const;
  DWORD   GetDataLen() const;
  DWORD   GetData(BYTE*& pWaveData, DWORD dwMaxToCopy) const;

  // Protected Methods
protected:
  BOOL    Free();
  // Private Data
private:
  BYTE* m_pImageData;
  DWORD m_dwImageLen;
  BOOL  m_bResource;
};


#endif

As you can see, the CWave class supports three constructors: a default constructor, a file constructor, and a resource constructor. The default constructor simply creates an empty CWave object with no actual wave data. The file constructor takes a wave filename as the only parameter and constructs a CWave object from the wave file. Finally, the resource constructor takes a module instance handle and a resource identifier as parameters and creates a CWave object from the resource image of the wave. The destructor for the CWave class is responsible for freeing the image data associated with the wave. This will make more sense in a moment.

The Create() member functions take on the task of reading a wave from a file or resource. Both Create() methods call the Free() method to free any previous wave data before loading a new wave. The IsValid() member function checks whether the object contains valid wave data.

The Play() member function is used to play a wave using the Win32 PlaySound() function. However, it doesn’t enter the picture when using the CWave object with DirectSound. For that, you must use the GetFormat(), GetDataLen(), and GetData() member functions. These member functions enable you to retrieve information about the format of the wave, the length of the raw wave data, and the raw wave data itself.

Listing 31.6 contains the definition of the CWave class, which shows how each of the member functions is implemented.

Listing 31.6 The Wave.cpp Source Code File for the CWave Class


//-----------------------------------------------------------------
// Inclusions
//-----------------------------------------------------------------
#include “StdAfx.h”
//-----------------------------------------------------------------
#include “Wave.h”
#include “MMIO.h”

//-----------------------------------------------------------------
// MFC Debugging Support
//-----------------------------------------------------------------
#ifdef _DEBUG
#undef THIS_FILE
static char BASED_CODE THIS_FILE[] = __FILE__;
#endif


//-----------------------------------------------------------------
// CWave Public Constructor(s)/Destructor
//-----------------------------------------------------------------
CWave::CWave()
  : m_dwImageLen(0), m_bResource(FALSE), m_pImageData(NULL) {
}

CWave::CWave(const CString& sFileName)
  : m_dwImageLen(0), m_bResource(FALSE), m_pImageData(NULL) {
  Create(sFileName);
}

CWave::CWave(UINT uiResID, HMODULE hmod)
  : m_dwImageLen(0), m_bResource(TRUE), m_pImageData(NULL) {
  Create(uiResID, hmod);
}

CWave::~CWave() {
  // Free the wave image data
  Free();
}

//-----------------------------------------------------------------
// CWave Public Methods
//-----------------------------------------------------------------
BOOL CWave::Create(const CString& sFileName) {
  // Free any previous wave image data
  Free();

  // Flag as regular memory
  m_bResource = FALSE;
  // Open the wave file
  CFile fileWave;
  if (!fileWave.Open(sFileName, CFile::modeRead))
    return FALSE;

  // Get the file length
  m_dwImageLen = fileWave.GetLength();

  // Allocate and lock memory for the image data
  m_pImageData = (BYTE*)::GlobalLock(::GlobalAlloc(GMEM_MOVEABLE |
    GMEM_SHARE, m_dwImageLen));
  if (!m_pImageData)
    return FALSE;

  // Read the image data from the file
  fileWave.Read(m_pImageData, m_dwImageLen);

  return TRUE;
}

BOOL CWave::Create(UINT uiResID, HMODULE hmod) {
  // Free any previous wave image data
  Free();

  // Flag as resource memory
  m_bResource = TRUE;

  // Find the wave resource
  HRSRC hresInfo;
  hresInfo = ::FindResource(hmod, MAKEINTRESOURCE(uiResID),
    “WAVE”);
  if (!hresInfo)
    return FALSE;

  // Load the wave resource
  HGLOBAL hgmemWave = ::LoadResource(hmod, hresInfo);

  if (hgmemWave) {
    // Get pointer to and length of the wave image data
    m_pImageData= (BYTE*)::LockResource(hgmemWave);
    m_dwImageLen = ::SizeofResource(hmod, hresInfo);
  }

  return (m_pImageData ? TRUE : FALSE);
}

BOOL CWave::Play(BOOL bAsync, BOOL bLooped) const {
  // Check validity
  if (!IsValid())
    return FALSE;

  // Play the wave
  return ::PlaySound((LPCSTR)m_pImageData, NULL, SND_MEMORY |
    SND_NODEFAULT | (bAsync ? SND_ASYNC : SND_SYNC) | (bLooped ?
    (SND_LOOP | SND_ASYNC) : 0));
}

BOOL CWave::GetFormat(WAVEFORMATEX& wfFormat) const {
  // Check validity
  if (!IsValid())
    return FALSE;

  // Set up and open the MMINFO structure
  CMMMemoryIOInfo mmioInfo((HPSTR)m_pImageData, m_dwImageLen);
  CMMIO           mmio(mmioInfo);

  // Find the WAVE chunk
  CMMTypeChunk mmckParent(‘W’,‘A’,‘V’,‘E’);
  mmio.Descend(mmckParent, MMIO_FINDRIFF);

  // Find and read the format subchunk
  CMMIdChunk mmckSubchunk(‘f’,‘m’,‘t’,‘ ‘);
  mmio.Descend(mmckSubchunk, mmckParent, MMIO_FINDCHUNK);
  mmio.Read((HPSTR)&wfFormat, sizeof(WAVEFORMATEX));
  mmio.Ascend(mmckSubchunk);
  return TRUE;
}

DWORD CWave::GetDataLen() const {
  // Check validity
  if (!IsValid())
    return (DWORD)0;

  // Set up and open the MMINFO structure
  CMMMemoryIOInfo mmioInfo((HPSTR)m_pImageData, m_dwImageLen);
  CMMIO           mmio(mmioInfo);

  // Find the WAVE chunk
  CMMTypeChunk mmckParent(‘W’,‘A’,‘V’,‘E’);
  mmio.Descend(mmckParent, MMIO_FINDRIFF);

  // Find and get the size of the data subchunk
  CMMIdChunk mmckSubchunk(‘d’,‘a’,‘t’,‘a’);
  mmio.Descend(mmckSubchunk, mmckParent, MMIO_FINDCHUNK);
  return mmckSubchunk.cksize;
}

DWORD CWave::GetData(BYTE*& pWaveData, DWORD dwMaxLen) const {
  // Check validity
  if (!IsValid())
    return (DWORD)0;
  // Set up and open the MMINFO structure
  CMMMemoryIOInfo mmioInfo((HPSTR)m_pImageData, m_dwImageLen);
  CMMIO           mmio(mmioInfo);

  // Find the WAVE chunk
  CMMTypeChunk mmckParent(‘W’,‘A’,‘V’,‘E’);
  mmio.Descend(mmckParent, MMIO_FINDRIFF);

  // Find and get the size of the data subchunk
  CMMIdChunk mmckSubchunk(‘d’,‘a’,‘t’,‘a’);
  mmio.Descend(mmckSubchunk, mmckParent, MMIO_FINDCHUNK);
  DWORD dwLenToCopy = mmckSubchunk.cksize;

  // Allocate memory if the passed in pWaveData was NULL
  if (pWaveData == NULL)
    pWaveData = (BYTE*)::GlobalLock(::GlobalAlloc(GMEM_MOVEABLE,
      dwLenToCopy));
  else
    // If you didn’t allocate your own memory, honor dwMaxLen
    if (dwMaxLen < dwLenToCopy)
      dwLenToCopy = dwMaxLen;
  if (pWaveData)
    // Read waveform data into the buffer
    mmio.Read((HPSTR)pWaveData, dwLenToCopy);

  return dwLenToCopy;
}

//-----------------------------------------------------------------
// CWave Protected Methods
//-----------------------------------------------------------------
BOOL CWave::Free() {
  // Free any previous wave data
  if (m_pImageData) {
    HGLOBAL  hgmemWave = ::GlobalHandle(m_pImageData);

    if (hgmemWave) {
      if (m_bResource)
        // Free resource (Win95 does NOT automatically do this)
        ::FreeResource(hgmemWave);
      else {
        // Unlock and free memory
        ::GlobalUnlock(hgmemWave);
        ::GlobalFree(hgmemWave);
      }

      m_pImageData = NULL;
      m_dwImageLen = 0;
      return TRUE;
    }
  }
  return FALSE;
}



The implementation of the CWave class reveals that the file- and resource-based constructors call the Create() member function to initialize the object. If you use the default constructor to create a CWave object, you must call Create() yourself in order to properly initialize the object. This is a common object initialization approach in MFC and is referred to as two-phase construction.

The implementation of the Play() member function shows how the PlaySound() Win32 API function is used to provide a high-level means of playing waves using the CWave class. You’re probably more interested in using the CWave class to play waves using DirectSound, however. More important to DirectSound are the GetFormat(), GetDataLen(), and GetData() member functions. These member functions use two multimedia support classes, CMMMemoryIOInfo and CMMIO, to retrieve information about the format of a wave and the raw data associated with a wave. This is necessary because DirectSound utilizes waves at a low level and must have access to raw wave data.

You probably noticed that the GetFormat(), GetDataLen(), and GetData() member functions operate on chunks of data. Chunks form the basis of RIFF files, which represent the format wave files are stored in. It isn’t terribly important that you understand the structure of RIFF files, but it is worth mentioning to help make the CWave code a little clearer. For more information on RIFF files and how to navigate through them, refer to the multimedia I/O data structures and functions in the Win32 API. On the other hand, you could just use the CWave class and not worry about the hassles of navigating RIFF files.

Now that you’ve seen how the CWave class is implemented, you’re probably curious to see how it works. Following is an example of playing a wave using the high-level Play() member function in the CWave class:

CWave wavGong(“Gong.wav”);
wavGong.Play();

Although the high-level approach to playing waves is certainly simple and effective, it doesn’t offer the power and flexibility of DirectSound. Let’s continue onward and see how to use the CWave class with DirectSound.

Revisiting DirectSound

Before getting into the code for using the CWave class with DirectSound, let’s take a moment to revisit DirectSound. If you recall from Chapter 30, DirectSound is implemented as a set of COM objects for representing both physical sound devices and sound data buffers. Following are the DirectSound objects involved in the playback of audio:

  DirectSound—Represents a physical hardware sound device
  DirectSoundBuffer—Represents an audio stream
  DirectSound3DBuffer—Represents an audio stream positioned in 3D space
  DirectSound3Dlistener—Represents an audio listener positioned in 3D space
  DirectSoundNotify—Provides a mechanism for notifying an application of DirectSound events

To keep things relatively simple, I’m going to stick with 2D audio and show how to mix 2D audio clips using DirectSound. To use 2D audio, you really only need to use the DirectSound and DirectSoundBuffer objects. So, let’s focus on these two objects and learn more about their role in DirectSound.

The DirectSound Object

The DirectSound object is a software representation of a physical audio hardware device (a sound card). Because most computers have only one sound card, you will typically use only one DirectSound object. This means that multiple applications that use DirectSound will have to share the DirectSound object because they all use the same physical audio hardware. Fortunately, DirectSound automatically tracks the input focus of each application and produces sound only for the application with input focus. In other words, you don’t have to worry about sharing the DirectSound object.

DirectSound objects are manipulated through the IDirectSound COM interface. You create a DirectSound object by calling the global DirectSoundCreate() function, which returns a pointer to an IDirectSound interface. The IDirectSound interface member functions are used to get and set DirectSound object attributes, as well as to create DirectSoundBuffer objects. Following are the member functions defined in the IDirectSound interface, some of which you’ll use a little later in the chapter:

  Initialize()
  SetCooperativeLevel()
  CreateSoundBuffer()
  DuplicateSoundBuffer()
  GetCaps()
  Compact()
  GetSpeakerConfig()
  SetSpeakerConfig()
  AddRef()
  QueryInterface()
  Release()

The DirectSoundBuffer Object

The DirectSoundBuffer object represents a stream of wave audio and comes in two forms: the primary sound buffer and secondary sound buffers. The primary sound buffer represents the audio buffer being played on the physical audio device. Secondary sound buffers represent individual audio streams that are mixed into the primary buffer for output. Figure 31.5 shows the relationship between the primary and secondary buffers.


Figure 31.5  The relationship between the primary and secondary sound buffers.

Figure 31.5 shows how the primary sound buffer represents the output of the DirectSound object and effectively serves as the result of mixing secondary buffers together. Secondary sound buffers are equivalent to the inputs on an audio mixer, except that in DirectSound there are no limits to the number of secondary sound buffers. Each secondary sound buffer is capable of being mixed with any other secondary buffer, with the resulting sound residing in the primary buffer for output.

Practically speaking, secondary sound buffers are used to represent each discrete sound in a multimedia application. You can create and mix as many secondary buffers as you need within a given application.

DirectSoundBuffer objects are manipulated through the IDirectSoundBuffer COM interface. You create DirectSoundBuffer objects by calling the CreateSoundBuffer() member function on the DirectSound object, which returns a pointer to an IDirectSoundBuffer interface. The IDirectSoundBuffer interface member functions are used to get and set DirectSoundBuffer object attributes, as well as to write audio data and play the sound buffers. Following are the member functions defined in the IDirectSoundBuffer interface, some of which you’ll use a little later in the chapter:

  GetCaps()
  GetFormat()
  SetFormat()
  GetStatus()
  Initialize()
  Restore()
  GetCurrentPosition()
  SetCurrentPosition()
  Lock()
  Unlock()
  Play()
  Stop()
  GetFrequency()
  SetFrequency()
  GetPan()
  SetPan()
  GetVolume()
  SetVolume()
  AddRef()
  QueryInterface()
  Release()



Static and Streaming Sound Buffers

It’s worth pointing out that the DirectSoundBuffer object supports both static and streaming sound buffers. A static sound buffer is a buffer that contains an entire sound, whereas a streaming sound buffer usually contains only part of a sound and requires the application to write new data to the sound buffer as the buffer is being played. Static buffers are more efficient because DirectSound will store them directly in the memory of a hardware audio device if possible. If a static buffer can be stored in the hardware audio device’s memory, the sound hardware takes on the task of mixing the audio, which is much faster than leaving it up to the system CPU. It is also possible to utilize hardware mixing with streaming sound buffers, provided that the system data bus is fast enough to transfer the stream of data to the audio hardware as it is delivered.

Unless you are building an application that pulls audio off the Internet, you will more than likely want to use static sound buffers because they are more efficient. It is important to point out that audio hardware memory is limited, which means that there might not be enough room for it to hold all the static sound buffers you are using. Therefore, you should prioritize sound buffers so that the most commonly played buffers have the best chance of being stored directly in audio hardware memory. You do this by simply creating and initializing the most commonly played sound buffers first.

Using DirectSound to Play Sound Effects

Although I could continue on with a theoretical discussion of DirectSound, you’re no doubt itching to see it in action. So, let’s take a look at a practical application that demonstrates some of the main features of DirectSound. The remainder of the chapter focuses on the design and development of the War application, which uses DirectSound to create an audio simulation of a military battlefield. The War application mixes sound effects such as gunfire and explosions at random intervals and with random volume and panning values. By randomly altering the volume and panning of each sound, War gives the effect of the sounds occurring at different spatial locations.


Note:  

Because the War application simulates sounds occurring at different spatial locations, it would make an ideal application for using DirectSound’s 3D audio features. However, I wanted to keep things relatively simple. Even so, you might be surprised at how effective it is to vary the volume and panning of sound effects to give the feel of a battlefield.


The first place to start with the War application is the application resources. Listing 31.7 contains the code for the Resource.h header file that defines resource identifiers for the application.

Listing 31.7 The Resource.h Header File for War


//-----------------------------------------------------------------
// Icons                    Range : 1000 - 1999
//-----------------------------------------------------------------
#define IDR_WAR             1000

//-----------------------------------------------------------------
// Waves                    Range : 4000 - 4999
//-----------------------------------------------------------------
#define IDW_HELICOPTER      4000
#define IDW_RICOCHET1       4001
#define IDW_RICOCHET2       4002
#define IDW_GUNFIRE1        4003
#define IDW_GUNFIRE2        4004
#define IDW_GUNFIRE3        4005
#define IDW_GUNFIRE4        4006
#define IDW_GUNFIRE5        4007
#define IDW_EXPLODE1        4008
#define IDW_EXPLODE2        4009
#define IDW_EXPLODE3        4010
#define IDW_EXPLODE4        4011
#define IDW_SIREN           4012
//-----------------------------------------------------------------
// Timers                   Range : 5000 - 5999
//-----------------------------------------------------------------
#define IDT_SOUND           5000

Most of the resource identifiers are used to identify the wave resources, which you’ll learn about in a moment. The other resource identifier of interest is IDT_SOUND, which identifies a timer used to play the different war sound effects. These resources are defined in the War.rc resource script, which is shown in Listing 31.8.

Listing 31.8 The War.rc Resource Script for War


//-----------------------------------------------------------------
// Inclusions
//-----------------------------------------------------------------
#include “AfxRes.h”
#include “Resource.h”

//-----------------------------------------------------------------
// Icons
//-----------------------------------------------------------------
IDR_WAR         ICON      DISCARDABLE   “War.ico”

//-----------------------------------------------------------------
// Waves
//-----------------------------------------------------------------
IDW_HELICOPTER  WAVE      DISCARDABLE   “Helicopter.wav”
IDW_RICOCHET1   WAVE      DISCARDABLE   “Ricochet1.wav”
IDW_RICOCHET2   WAVE      DISCARDABLE   “Ricochet2.wav”
IDW_GUNFIRE1    WAVE      DISCARDABLE   “GunFire1.wav”
IDW_GUNFIRE2    WAVE      DISCARDABLE   “GunFire2.wav”
IDW_GUNFIRE3    WAVE      DISCARDABLE   “GunFire3.wav”
IDW_GUNFIRE4    WAVE      DISCARDABLE   “GunFire4.wav”
IDW_GUNFIRE5    WAVE      DISCARDABLE   “GunFire5.wav”
IDW_EXPLODE1    WAVE      DISCARDABLE   “Explode1.wav”
IDW_EXPLODE2    WAVE      DISCARDABLE   “Explode2.wav”
IDW_EXPLODE3    WAVE      DISCARDABLE   “Explode3.wav”
IDW_EXPLODE4    WAVE      DISCARDABLE   “Explode4.wav”
IDW_SIREN       WAVE      DISCARDABLE   “Siren.wav”

//-----------------------------------------------------------------
// Strings
//-----------------------------------------------------------------
STRINGTABLE PRELOAD DISCARDABLE
BEGIN
  AFX_IDS_APP_TITLE “War”
END

Beyond defining an icon and an application title string, the War.rc resource script defines resources for each of the wave sound effects used in the application. The WAVE resource type is used to define these wave resources.

The CMainFrame class in the War application represents the main application frame window and also takes on the responsibility of managing all DirectSound operations. Listing 31.9 contains the source code for the MainFrame.h header file, which declares the CMainFrame class.

Listing 31.9 The MainFrame.h Header File for War


#ifndef __MAINFRAME_H__
#define __MAINFRAME_H__

//-----------------------------------------------------------------
// Inclusions
//-----------------------------------------------------------------
#include <MMSystem.h>
#include <DSound.h>

//-----------------------------------------------------------------
// Defines
//-----------------------------------------------------------------
#define NUMSOUNDS 13

//-----------------------------------------------------------------
// CMainFrame Class - Main Frame Window Object
//-----------------------------------------------------------------
class CMainFrame : public CFrameWnd {
  // Private Data
private:
  LPDIRECTSOUND       m_pDirectSound;
  LPDIRECTSOUNDBUFFER m_pDSBuffer[NUMSOUNDS];
  UINT                m_uiTimerID;
  LONG                m_lHeliPan;
  LONG                m_lHeliPanInc;

  // Public Constructor(s)/Destructor
public:
                      CMainFrame();
  virtual             ~CMainFrame();

  // Public Methods
public:
  BOOL                Create(const CString& sTitle);

  // Protected Methods
public:
  BOOL                InitDirectSound();
  void                CleanupDirectSound();
  BOOL                InitDSBuffers();
  BOOL                RestoreDSBuffers();

  // Message Handler Methods
protected:
  afx_msg int         OnCreate(LPCREATESTRUCT lpCreateStruct);
  afx_msg void        OnDestroy();
  afx_msg void        OnPaint();
  afx_msg void        OnTimer(UINT nIDEvent);

  // Message Map & Runtime Support
protected:
  DECLARE_MESSAGE_MAP()
  DECLARE_DYNCREATE(CMainFrame)
};

#endif



The CMainFrame class declares some member variables for managing DirectSound objects and tasks. More specifically, the m_pDirectSound variable stores a pointer to the DirectSound object. The m_pDSBuffer array stores an array of pointers to the DirectSound buffers for each sound effect. The m_uiTimerID variable stores the identifier of the timer, which generates timing events that are used to play sounds. The m_lHeliPan and m_lHeliPanInc variables are used to control the panning of the helicopter sound effect, which is moved from right to left or left to right between speakers when played.

The protected member functions in the CMainFrame class are used to initialize, clean up, and manage DirectSound objects. The InitDirectSound() member function creates a DirectSound object and initializes DirectSound buffers, and the CleanupDirectSound() member function frees memory associated with the DirectSound object and sound buffers. The InitDSBuffers() member function is called by InitDirectSound() to create and initialize a sound buffer for each sound effect wave. It is possible for the memory associated with DirectSound buffers to be lost, in which case you must restore the buffers before attempting to play them. The RestoreDSBuffers() member function restores the memory for the sound effect buffers and reinitializes them with wave data.

The other notable member function in the CMainFrame class is OnTimer(), which is actually a message handler. The OnTimer() message handler is called in response to a timer event, which is set in the War application to occur once every half second. The OnTimer() message handler is responsible for playing random sounds with a random volume and panning.

To understand how the CMainFrame member functions are implemented, you must look to the CMainFrame class definition, which is shown in Listing 31.10.

Listing 31.10 The MainFrame.cpp Source Code File for War


//-----------------------------------------------------------------
// Inclusions
//-----------------------------------------------------------------
#include “StdAfx.h”
//-----------------------------------------------------------------
#include “MainFrame.h”
#include “Wave.h”
#include “Resource.h”
#include <StdLib.h>

//-----------------------------------------------------------------
// MFC Debugging Support
//-----------------------------------------------------------------
#ifdef _DEBUG
#undef THIS_FILE
static char BASED_CODE THIS_FILE[] = __FILE__;
#endif


//-----------------------------------------------------------------
// CMainFrame Message Map & Runtime Support
//-----------------------------------------------------------------
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
  ON_WM_CREATE()
  ON_WM_DESTROY()
  ON_WM_PAINT()
  ON_WM_TIMER()
END_MESSAGE_MAP()

IMPLEMENT_DYNCREATE(CMainFrame, CFrameWnd)

//-----------------------------------------------------------------
// CMainFrame Public Constructor(s)/Destructor
//-----------------------------------------------------------------
CMainFrame::CMainFrame()
  : m_pDirectSound(NULL), m_uiTimerID(0), m_lHeliPan(0),
    m_lHeliPanInc(0) {
  // Initialize member variables
  for (int i = 0; i < NUMSOUNDS; i++)
    m_pDSBuffer = NULL;
}

CMainFrame::~CMainFrame() {
  CleanupDirectSound();
}

//-----------------------------------------------------------------
// Public Methods
//-----------------------------------------------------------------
BOOL CMainFrame::Create(const CString& sTitle) {
  CString sClassName;

  sClassName = AfxRegisterWndClass(CS_HREDRAW | CS_VREDRAW,
    LoadCursor(NULL, IDC_ARROW),
    (HBRUSH)(COLOR_WINDOW + 1),
    LoadIcon(AfxGetInstanceHandle(),
    MAKEINTRESOURCE(IDR_WAR)));

  return CFrameWnd::Create(sClassName, sTitle);
}

//-----------------------------------------------------------------
// Protected Methods
//-----------------------------------------------------------------
BOOL CMainFrame::InitDirectSound() {
  // Create the DS object
  if (::DirectSoundCreate(NULL, &m_pDirectSound, NULL) != DS_OK) {
    MessageBox(“Could not create DirectSound object!”,
      “DirectSound Error”);
    CleanupDirectSound();
    return FALSE;
  }

  // Set the cooperation level for the DS object
  if (m_pDirectSound->SetCooperativeLevel(GetSafeHwnd(),
    DSSCL_NORMAL) != DS_OK) {
    MessageBox(“Could not set cooperative level!”,
      “DirectSound Error”);
    CleanupDirectSound();
    return FALSE;
  }

  // Initialize the DS buffers
  if (!InitDSBuffers()) {
    MessageBox(“Could not initialize DirectSound buffers!”,
      “DirectSound Error”);
    CleanupDirectSound();
    return FALSE;
  }

  return TRUE;
}

void CMainFrame::CleanupDirectSound() {
  // Cleanup the DS object
  if (m_pDirectSound) {
    m_pDirectSound->Release();
    m_pDirectSound = NULL;
  }
}

BOOL CMainFrame::InitDSBuffers() {
  // Initialize waves
  CWave waves[NUMSOUNDS];
  waves[0].Create(IDW_HELICOPTER);
  waves[1].Create(IDW_RICOCHET1);
  waves[2].Create(IDW_RICOCHET2);
  waves[3].Create(IDW_GUNFIRE1);
  waves[4].Create(IDW_GUNFIRE2);
  waves[5].Create(IDW_GUNFIRE3);
  waves[6].Create(IDW_GUNFIRE4);
  waves[7].Create(IDW_GUNFIRE5);
  waves[8].Create(IDW_EXPLODE1);
  waves[9].Create(IDW_EXPLODE2);
  waves[10].Create(IDW_EXPLODE3);
  waves[11].Create(IDW_EXPLODE4);
  waves[12].Create(IDW_SIREN);

  // Initialize DS buffers
  for (int i = 0; i < NUMSOUNDS; i++) {
    // Get the wave information
    DWORD         dwDataLen = waves.GetDataLen();
    WAVEFORMATEX  wfFormat;
    waves.GetFormat(wfFormat);

    // Set up the DS buffer description
    DSBUFFERDESC  dsbdDesc;
    ZeroMemory(&dsbdDesc, sizeof(DSBUFFERDESC));
    dsbdDesc.dwSize = sizeof(DSBUFFERDESC);
    dsbdDesc.dwFlags = DSBCAPS_CTRLDEFAULT | DSBCAPS_STATIC;
    dsbdDesc.dwBufferBytes = dwDataLen;
    dsbdDesc.lpwfxFormat = &wfFormat;

    // Create the DS buffer
    if (m_pDirectSound->CreateSoundBuffer(&dsbdDesc,
      &m_pDSBuffer, NULL) != DS_OK)
      return FALSE;

    // Lock the DS buffer
    BYTE* pDSBuffData;
    if (m_pDSBuffer->Lock(0, dwDataLen, (LPLPVOID)&pDSBuffData,
      &dwDataLen, NULL, 0, 0) != DS_OK)
      return FALSE;

    // Write wave data to the DS buffer
    dwDataLen = waves.GetData(pDSBuffData, dwDataLen);

    // Unlock the DS buffer
    if (m_pDSBuffer->Unlock(pDSBuffData, dwDataLen, NULL, 0) !=
      DS_OK)
      return FALSE;
  }

  return TRUE;
}

BOOL CMainFrame::RestoreDSBuffers() {
  // Restore the buffers
  for (int i = 0; i < NUMSOUNDS; i++)
    if (m_pDSBuffer->Restore() != DS_OK)
      return FALSE;

  // Reinitialize the buffers
  return InitDSBuffers();
}

//-----------------------------------------------------------------
// CMainFrame Message Handler Methods
//-----------------------------------------------------------------
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) {
  // Initialize DirectSound
  if (!InitDirectSound())
    return -1;

  // Seed the random number generator
  ::srand((UINT)timeGetTime());

  // Set the timer
  m_uiTimerID = SetTimer(IDT_SOUND, 500, NULL);
  if (m_uiTimerID == 0)
    return -1;

  return 0;
}

void CMainFrame::OnDestroy() {
  // Kill the timer
  KillTimer(m_uiTimerID);
}

void CMainFrame::OnPaint() {
  // Get the DC
  CPaintDC dc(this);

  // Calculate where to draw the text
  CString sText(“Close your eyes, crank the volume, “
    “and hunker down for war...”);
  CRect   rcClient;
  CSize   sizText;
  CPoint  ptTextPos;
  GetClientRect(&rcClient);
  sizText = dc.GetTextExtent(sText, sText.GetLength());
  ptTextPos.x = (rcClient.Width() - sizText.cx) / 2;
  ptTextPos.y = (rcClient.Height() - sizText.cy) / 2;

  // Draw the text
  dc.TextOut(ptTextPos.x, ptTextPos.y, sText);
}

void CMainFrame::OnTimer(UINT nIDEvent) {
  if (nIDEvent == m_uiTimerID) {
    // See if you should play a sound (50-50 chance)
    if (::rand() % 2) {
      // Determine which sound to play
      int nIndex = ::rand() % NUMSOUNDS;
      if (m_pDSBuffer[nIndex] != NULL) {
        DWORD dwStatus;
        m_pDSBuffer[nIndex]->GetStatus(&dwStatus);

        // Check to make sure that the buffer hasn’t been lost
        if (dwStatus & DSBSTATUS_BUFFERLOST)
          RestoreDSBuffers();

        // Check to make sure that the sound isn’t already playing
        if (!(dwStatus & DSBSTATUS_PLAYING)) {
          if (nIndex > 0) {
            // Set the panning of the sound
            m_pDSBuffer[nIndex]->SetPan((::rand() % 2000) - 1000);

            // Set the volume of the sound
            m_pDSBuffer[nIndex]->SetVolume((::rand() % 3) * -250);

            // Play the sound
            m_pDSBuffer[nIndex]->Play(0, 0, 0);
          }
          else {
            // Set the panning of the helicopter
            LONG lStart = (::rand() % 2) ? -1 : 1;
            m_lHeliPan = lStart * 8000;
            m_lHeliPanInc = -(lStart * 400);
            m_pDSBuffer[nIndex]->SetPan(m_lHeliPan);

            // Play the helicopter sound
            m_pDSBuffer[nIndex]->Play(0, 0, DSBPLAY_LOOPING);
          }
        }
      }
    }
    // If helicopter sound is playing, see if you should stop it
    DWORD dwStatus;
    m_pDSBuffer[0]->GetStatus(&dwStatus);
    if (dwStatus & DSBSTATUS_LOOPING) {
      if ((m_lHeliPan < -8000) || (m_lHeliPan > 8000))
        m_pDSBuffer[0]->Stop();
      else {
        m_lHeliPan += m_lHeliPanInc;
        m_pDSBuffer[0]->SetPan(m_lHeliPan);
        m_pDSBuffer[0]->SetVolume(-(::abs(m_lHeliPan) / 5));
      }
    }
  }
}



The implementation of the CMainFrame class reveals some interesting things about how DirectSound is used to mix audio clips. The InitDirectSound() member function first calls the DirectSoundCreate() function to create a DirectSound object. The cooperative level of the DirectSound object is then set with a call to SetCooperativeLevel(). Setting the cooperative level is a strict requirement before using a DirectSound object. In this case, the cooperative level is set to DSSCL_NORMAL, which provides the smoothest multitasking and resource-sharing behavior for the DirectSound object. The last step in the InitDirectSound() member function is initializing the DirectSound buffers with a call to InitDSBuffers(), which you’ll learn about in a moment.

The CleanupDirectSound() member function is responsible for releasing the DirectSound object, which also releases the DirectSound buffers associated with the object.

The InitDSBuffers() member function creates and initializes the DirectSound sound effect buffers. This is where the CWave class enters the picture with DirectSound. The CWave class includes support member functions necessary to handle creating a sound buffer of the correct size and with the correct wave format, along with copying the wave data into the buffer. InitDSBuffers() creates an array of CWave objects as a means of initializing DirectSound buffers. The CWave objects are created from wave resources defined in the War application’s resource script. The static DirectSound buffers are then created based on each of the CWave objects.

The RestoreDSBuffers() member function is used to restore DirectSound buffers in the event that their memory is lost. RestoreDSBuffers() simply calls the Restore() member function on each buffer and then reinitializes them with a call to InitDSBuffers().

With the DirectSound member functions in place, the other major part of the CMainFrame class to address is the timing mechanism used to play the sound effects. The OnCreate() message handler establishes the timing mechanism by setting up a timer that generates timer events at half-second (500-millisecond) intervals. The timer is destroyed in the OnDestroy() message handler.

The OnTimer() message handler is called every half second in response to timer events and is ultimately where the fun lies in the War application. The OnTimer() message handler first checks whether a sound effect should be played, which is given a 50-50 likelihood of happening. If a sound effect is to be played, OnTimer() then randomly selects one of the sound effects from the array of DirectSound buffers. The GetStatus() member function is then called on the selected sound buffer to make sure that the buffer hasn’t been lost. If it has been lost, the RestoreDSBuffers() member function is called to restore all the sound buffers. OnTimer() also checks to make sure that the selected sound buffer isn’t already playing.

The remainder of the code in OnTimer() sets the volume and panning of the sound buffer and then plays it. If the sound buffer isn’t the helicopter sound effect, random panning and volume are set, and the sound buffer is started playing. However, if the buffer is the helicopter sound effect, the panning and volume are specially set so that the helicopter sounds like it is flying by. This also requires OnTimer() to update the settings of the helicopter sound effect buffer periodically, which is reflected by the last block of code in the OnTimer() message handler. The helicopter sound effect is the only one that is played looped, which means that it is played repeatedly until it cycles from one speaker to the other and fades out. This logic is entirely controlled by the OnTimer() message handler.

Figure 31.6 shows the completed War application, which admittedly isn’t very revealing because this book isn’t capable of playing the audio in the application.


Figure 31.6  The completed War application.

Summary

This chapter introduces you to MFC multimedia programming, which encompasses a lot of territory. As you learned, multimedia programming under Windows really translates into DirectX programming because DirectX forms the basis for all the latest Windows multimedia technologies. This chapter shows you how to use the DirectX Media Player control, which is an ActiveX control that supports the playback of a wide range of streaming and nonstreaming media content. You built an application that utilized the Media Player control to serve as a media viewer.

The chapter also focuses on DirectSound, the high-performance audio portion of DirectX. You learned how to use DirectSound to mix wave audio clips and tightly control certain aspects of their playback, such as volume and panning. You also created an all-purpose MFC wave class that encapsulates the functionality of a wave audio clip.